Praca domowa 2

Urbala Anna

Dla wybranej obserwacji ze zbioru danych wylicz predykcję modelu.

In [1]:
# załadowanie modelu
import pickle
model = pickle.load(open("../../../../WB-XAI-Projekt/RF_model", "rb"))
In [2]:
import pandas as pd
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import LabelEncoder, OneHotEncoder
from sklearn.impute import SimpleImputer
from sklearn.model_selection import train_test_split


# Wczytanie i przygotowanie danych 
full_data = pd.read_csv("hotel_bookings.csv")
full_data["agent"] = full_data["agent"].astype(str)
treshold = 0.005 * len(full_data)
agents_to_change = full_data['agent'].value_counts()[full_data['agent'].value_counts() < treshold].index
full_data.loc[full_data["agent"].isin(agents_to_change), "agent"] = "other"

countries_to_change = full_data['country'].value_counts()[full_data['country'].value_counts() < treshold].index
full_data.loc[full_data["country"].isin(countries_to_change), "country"] = "other"


# Określenie cech uwzględnionych w modelu
num_features = ["lead_time", "arrival_date_week_number",
                "stays_in_weekend_nights", "stays_in_week_nights", 
                "adults", "previous_cancellations",
                "previous_bookings_not_canceled",
                "required_car_parking_spaces", "total_of_special_requests", 
                "adr", "booking_changes"]

cat_features = ["hotel", "market_segment", "country", 
                "reserved_room_type",
                "customer_type", "agent"]

features = num_features + cat_features

# Podział na zmienne wyjaśniające i target
X = full_data.drop(["is_canceled"], axis=1)[features]
y = full_data["is_canceled"]

categorical_names = {}
for feature in cat_features:
    col = X[[feature]]
    cat_transformer = SimpleImputer(strategy="constant", fill_value="Unknown")
    col = cat_transformer.fit_transform(col)
    X[feature] = col
    le = LabelEncoder()
    le.fit(X[[feature]])
    X[[feature]] = le.transform(X[[feature]])
    categorical_names[feature] = le.classes_

categorical_names
# Preprocessing
num_transformer = SimpleImputer(strategy="constant")

preprocessor = ColumnTransformer(transformers=[("num", num_transformer, num_features)],
                                remainder = 'passthrough')

for feature in num_features:
    X[feature] = X[feature].astype(float)

X_train, X_test, y_train, y_test = train_test_split(
    X, y,
    test_size=0.2, random_state=42)
/home/anna/.local/lib/python3.6/site-packages/sklearn/utils/validation.py:63: DataConversionWarning: A column-vector y was passed when a 1d array was expected. Please change the shape of y to (n_samples, ), for example using ravel().
  return f(*args, **kwargs)
/home/anna/.local/lib/python3.6/site-packages/sklearn/utils/validation.py:63: DataConversionWarning: A column-vector y was passed when a 1d array was expected. Please change the shape of y to (n_samples, ), for example using ravel().
  return f(*args, **kwargs)
/home/anna/.local/lib/python3.6/site-packages/sklearn/utils/validation.py:63: DataConversionWarning: A column-vector y was passed when a 1d array was expected. Please change the shape of y to (n_samples, ), for example using ravel().
  return f(*args, **kwargs)
/home/anna/.local/lib/python3.6/site-packages/sklearn/utils/validation.py:63: DataConversionWarning: A column-vector y was passed when a 1d array was expected. Please change the shape of y to (n_samples, ), for example using ravel().
  return f(*args, **kwargs)
/home/anna/.local/lib/python3.6/site-packages/sklearn/utils/validation.py:63: DataConversionWarning: A column-vector y was passed when a 1d array was expected. Please change the shape of y to (n_samples, ), for example using ravel().
  return f(*args, **kwargs)
/home/anna/.local/lib/python3.6/site-packages/sklearn/utils/validation.py:63: DataConversionWarning: A column-vector y was passed when a 1d array was expected. Please change the shape of y to (n_samples, ), for example using ravel().
  return f(*args, **kwargs)
In [3]:
selected_X = X_train.iloc[[13]]
selected_y = y_train.iloc[13]
predicted_y = model.predict(selected_X)
print("Prawdziwa wartość:", selected_y)
print("Przewidziana wartość:", predicted_y)
selected_X.head()
Prawdziwa wartość: 0
Przewidziana wartość: [0]
Out[3]:
lead_time arrival_date_week_number stays_in_weekend_nights stays_in_week_nights adults previous_cancellations previous_bookings_not_canceled required_car_parking_spaces total_of_special_requests adr booking_changes hotel market_segment country reserved_room_type customer_type agent
16046 41.0 33.0 2.0 1.0 2.0 0.0 0.0 0.0 1.0 190.5 0.0 1 6 16 0 2 7

Nasz model po raz kolejny poprawnie przewidział wartość (mimo że został odrobinę zmieniony od poprzedniej pracy domowej).

Dla wybranej obserwacji z punktu 1., wylicz dekompozycję predykcji modelu używając LIME (pakiety w R: live, lime, localModel, iml, pakiety w Python: lime, dalex)

In [4]:
import dalex as dx
explainer = dx.Explainer(model, X_train, y_train, label = "Random Forest")
Preparation of a new explainer is initiated

  -> data              : 95512 rows 17 cols
  -> target variable   : Parameter 'y' was a pandas.Series. Converted to a numpy.ndarray.
  -> target variable   : 95512 values
  -> model_class       : sklearn.ensemble._forest.RandomForestClassifier (default)
  -> label             : Random Forest
  -> predict function  : <function yhat_proba_default at 0x7ff1c8162730> will be used (default)
  -> predict function  : Accepts only pandas.DataFrame, numpy.ndarray causes problems.
  -> predicted values  : min = 0.0, mean = 0.371, max = 1.0
  -> model type        : classification will be used (default)
  -> residual function : difference between y and yhat (default)
  -> residuals         : min = -0.943, mean = -0.00218, max = 0.957
  -> model_info        : package sklearn

A new explainer has been created!
In [5]:
ps = explainer.predict_surrogate(selected_X, type = "lime",
                                 categorical_names=categorical_names,
                                 categorical_features=range(11, 17),
                                 show_all=True)
In [6]:
ps.show_in_notebook()
In [9]:
def print_cat(selected_X):
    print("country:", categorical_names["country"][selected_X["country"]])
    print("customer_type:", categorical_names["customer_type"][selected_X["customer_type"]])
    print("market_segment:", categorical_names["market_segment"][selected_X["market_segment"]])
    print("agent:", categorical_names["agent"][selected_X["agent"]])

print_cat(selected_X)
country: ['PRT']
customer_type: ['Transient']
market_segment: ['Online TA']
agent: ['241.0']

Największy wpływ na uzyskaną wartość (czyli brak anulowania rezerwacji) miała informacja o braku wcześniejszych anulowań. To, liczba specjalnych żądań oraz agent miało decydujący wpływ na uzyskany wynik (wszystko inne skłaniało ku rezygnacji). Zaskoczyło mnie, jak bardzo brak wcześniejszej anulacji wpłynął na predykcję - w poprzednim zadaniu domowym (break down) dla tej obserwacji to była dość ważna, ale zdecydowanie nie najważniejsza zmienna. Co prawda zmieniliśmy odrobinę model, ale skala i tak robi wrażenie.

Porównaj dekompozycję LIME dla różnych obserwacji w zbiorze. Jak stabilne są otrzymane wyjaśnienia?

Do wykonania tej części użyjemy obserwacji z poprzedniej pracy domowej. (Jeszcze raz dodam, że zmieniliśmy model - zmniejszyliśmy liczbę zmiennych w modelu i ze względu na sposób działania LIME zmieniliśmy OneHotEncoding na LabelEncoding).

In [7]:
X1 = X_train.iloc[[2137]]
X2 = X_train.iloc[[420]]

ps = explainer.predict_surrogate(X1, type = "lime",
                                 categorical_names=categorical_names,
                                 categorical_features=range(11, 17),
                                 show_all=True)
ps.show_in_notebook()
In [10]:
print_cat(X1)
country: ['other']
customer_type: ['Transient-Party']
market_segment: ['Offline TA/TO']
agent: ['other']

W przypadku tej obserwacji znowu ogromny wpływ na przewidzenie braku anulowania rezerwacji miał wpływ brak wcześniejszego anulowania (trudno napisać to tak, żeby brzmiało po polsku :)). Jedyne inne zmienne, które tu mają wpływ i są takie same dla obu obserwacji to required_car_parking_spaces, previous_bookings_not_canceled i booking_changes. W obu predykcjach zadziałały w tym samym kierunku (czyli w kierunku rezygnacji) i ze zbliżonymi wartościami (choć mimo wszystko nieco innymi).

Znowu previous_cancellation ma znacznie większy wpływ dla tej obserwacji niż w przypadku metody break down, ale jednak widać, że zmienne istotne dla break down (tj. customer_type, total_of_special_requests) tu też osiągają dość wysokie wartości (choć jak widać total_of_special_requests nie jest kluczowe, skoro prawdopodobieństwo braku anulowania rezerwacji wynosi 100%).

In [11]:
ps = explainer.predict_surrogate(X2, type = "lime",
                                 categorical_names=categorical_names,
                                 categorical_features=range(11, 17),
                                 show_all=True)
ps.show_in_notebook()
In [12]:
print_cat(X2)
country: ['other']
customer_type: ['Transient']
market_segment: ['Online TA']
agent: ['7.0']

W przypadku tej obserwacji znowu previous_cancellations jest kluczowe. Większość z pozostałych zmiennych już wystapiła przy poprzednich obserwacjach i znowu ma podobny wpływ. Ciekawe jest, że wśród tych 10 zmiennych aż 7 przyjęło taką samą wartość jak dla obserwacji pierwszej (tj.previous_cancellations, required_car_parking_spaces, customer_type, total_of_special_requests, market_segment, booking_changes, previous_bookings_not_canceled), a jednak prawdopodobieństwo braku anulacji jest znacznie wyższe - graniczące z pewnością (hehe, może to ze względu na kraj "inny", analogicznie jak w drugiej obserwacji ;)).

Spróbujemy jeszcze dodatkowo znaleźć choć jedną obserwację, dla której mamy predykcję anulowania rezerwacji.

In [14]:
X3 = X_train.iloc[[2]]

ps = explainer.predict_surrogate(X3, type = "lime",
                                 categorical_names=categorical_names,
                                 categorical_features=range(11, 17),
                                 show_all=True)
ps.show_in_notebook()
In [15]:
print_cat(X3)
country: ['ESP']
customer_type: ['Transient']
market_segment: ['Online TA']
agent: ['9.0']

Znowu previous_cancellations=0 zwiększa prawdopodobieństwo braku anulowania rezygnacji, ale już nie jest kluczowe. Ogólnie wartości dla tych samych zmiennych mają wpływ w tę samą stronę z podobną wartością, więc po porównaniu tych 4 obserwacji można uznać, że LIME jest dość stabilną metodą (przynajmniej w porównaniu do break down).

Ogólne wnioski:

  • LIME jest wystarczająco stabilny
  • wykresy są dość czytelne, choć czasem obcięte linijki nie pomagają w odczycie - na szczęście tabelka z Feature i Value bywa pomocna (coś mi nie wyszło wypisywanie wartości zmiennych kategorycznych w sposób czytelny na wykresie, dlatego dodałam metodę print_cat, ale nie przeszkadza to w interpretacji wykresów)
  • trochę nieintuicyjne jest od ilu "pomarańczowych" wartości predykcja będzie też "pomarańczowa", szczególnie widać to w przypadku pierwszej obserwacji, gdzie tylko 3 cechy świadczyły przeciwko anulowaniu (przy ostatniej obserwacji też 3 były przeciw i to z większym wpływem, a jednak predykcja jest za anulacją)
  • LIME wymaga zmiany modelu z one hot encodingu na label encoding, co dla osoby nieświadomej problemu może być nieoczywiste ze względu na mało intuicyjne błędy
In [ ]: